﻿using System;
using System.Collections.Generic;
using Dsl.Ped.Controls;
using Dsl.Ped.Controls.ProjectiveControl;
using System.Collections.ObjectModel;
using Dsl.Ped.Framework.Exceptions;
using Dsl.Ped.Framework.Common;

namespace Dsl.Ped.Framework.WPFControls.ProjectiveObject
{
    /// <summary>
    /// Two way adapter adopting the visual part (<see cref="ProjectionControl"/>) and business part (<see cref="IProjective"/>) between
    /// each other.
    /// </summary>
    public class ProjectiveControlAdapter
    {
        #region Fields

        /// <summary>
        /// <see cref="Collection{T}"/> of <see cref="IProjectionContainer"/>. The <see cref="ProjectiveObjectUserControl"/> can exist in multiple 
        /// instances for one <see cref="IProjective"/> object. We must keep those instances.
        /// </summary>
        private readonly List<IProjectionContainer> projectionContainersRepository;

        /// <summary>
        /// Instances <see cref="ProjectionContainerControl"/> associated with the given <see cref="IProjective"/> object
        /// </summary>
        private readonly List<ProjectionContainerControl> projectionContainerControlsRepository;

        /// <summary>
        /// Dictionary for optimization
        /// </summary>
        private Dictionary<IProjectionContainer, ProjectionContainerControl> container2ControlDictionary;

        /// <summary>
        /// Dictionary for optimization
        /// </summary>
        private Dictionary<ProjectionContainerControl, IProjectionContainer> control2ContainerDictionary;

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="ProjectiveControlAdapter"/> class. 
        /// </summary>
        /// <param name="projectiveObject">Projective object</param>
        public ProjectiveControlAdapter(IProjective projectiveObject)
        {
            this.projectionContainersRepository = projectiveObject.GetProjectionContainers();
            this.projectionContainerControlsRepository = this.GetProjectionContainerControls(this.projectionContainersRepository);

            this.InitializeDictionaries();
        }

        #endregion

        #region Members

        /// <summary>
        /// Gets the collection of <see cref="ProjectionContainerControl"/> instances of the <see cref="ProjectiveControl"/>
        /// </summary>
        public IEnumerable<ProjectionContainerControl> ProjectionContainerControls
        {
            get
            {
                return this.projectionContainerControlsRepository;
            }
        }

        /// <summary>
        /// Gets the collection of <see cref="IProjectionContainer"/> instances of the <see cref="IProjective"/>
        /// </summary>
        public List<IProjectionContainer> ProjectionContainers
        {
            get
            {
                return this.projectionContainersRepository;
            }
        }

        #endregion

        /// <summary>
        /// Get the <see cref="IProjectionContainer"/> corresponding to the given <see cref="projectionContainerControl"/>
        /// </summary>
        /// <param name="projectionContainerControl">The projection container control.</param>
        /// <returns>
        /// The <see cref="IProjectionContainer"/> corresponding to the <see cref="projectionContainerControl"/> </returns>
        public IProjectionContainer GetProjectionContainerControl(ProjectionContainerControl projectionContainerControl)
        {
            // If the container is not specified return null
            if (projectionContainerControl == null) return null;

            IProjectionContainer result;
            return this.control2ContainerDictionary.TryGetValue(projectionContainerControl, out result) ? result : null;
        }

        /// <summary>
        /// Get the <see cref="ProjectionContainerControl"/> associated with the given <see cref="projectionContainer"/>
        /// </summary>
        /// <param name="projectionContainer">The projection container.</param>
        /// <returns>The <see cref="ProjectionContainerControl"/>.</returns>
        public ProjectionContainerControl GetProjectionContainerControl(IProjectionContainer projectionContainer)
        {
            ProjectionContainerControl result;
            if (!projectionContainer.ForceRefresh)
            {
                return this.container2ControlDictionary.TryGetValue(projectionContainer, out result) ? result : null;
            }
            else
            {
                return this.RefreshProjectionContainerControl(projectionContainer);
            }
        }

        /// <summary>
        /// Refresh given <see cref="ProjectionContainerControl"/> that corresponds with the <see cref="IProjectionContainer"/>.
        /// Each projection whose content implements <see cref="IRefreshableControl"/> will be refreshed via a method <see cref="IRefreshableControl.Refresh"/>.
        /// </summary>
        /// <param name="projectionContainer">ProjectionContainer whose Control we need to refresh</param>
        /// <returns><see cref="ProjectionContainerControl"/> that was refreshed</returns>
        private ProjectionContainerControl RefreshProjectionContainerControl(IProjectionContainer projectionContainer)
        {
            try
            {
                ProjectionContainerControl result;

                var containerControl = this.container2ControlDictionary.TryGetValue(projectionContainer, out result) ? result : null;
                if (containerControl != null)
                {
                    foreach (var projection in containerControl.Items)
                    {
                        if (projection is ProjectionControl)
                        {
                            var projectionControl = projection as ProjectionControl;
                            if (projectionControl.Content is IRefreshableControl)
                            {
                                var refreshableControl = projectionControl.Content as IRefreshableControl;
                                refreshableControl.Refresh();
                            }
                        }
                    }
                }
                return containerControl;
            }
            catch (Exception)
            {
                return null;
            }
        }

        /// <summary>
        /// Get the collection of <see cref="ProjectionContainer"/> controls out of the collection of <see cref="IProjectionContainer"/> object
        /// specified for some <see cref="IProjective"/> object.
        /// </summary>
        /// <param name="projectionContainers">Collection of business <see cref="IProjectionContainer"/> objects</param>
        /// <returns>Collection of <see cref="ProjectionContainers"/> controls corresponding to the given collection of <see cref="IProjectionContainer"/> instances</returns>
        private List<ProjectionContainerControl> GetProjectionContainerControls(IEnumerable<IProjectionContainer> projectionContainers)
        {
            var projectionContainerControls = new List<ProjectionContainerControl>();

            foreach (var projectionContainer in projectionContainers)
            {
                var projectionContainerControl = this.InitializeProjectionContainerControl(projectionContainer);
                projectionContainerControls.Add(projectionContainerControl);
            }

            return projectionContainerControls;
        }

        /// <summary>
        /// Initialize <see cref="ProjectionContainerControl"/> for the given instance of <see cref="IProjectionContainer"/>
        /// </summary>
        /// <param name="projectionContainer">ProjectionContainer, for which we want to initialize a <see cref="ProjectionContainerControl"/></param>
        /// <returns>new instance of <see cref="ProjectionContainerControl"/> that was created for given <see cref="projectionContainer"/></returns>
        private ProjectionContainerControl InitializeProjectionContainerControl(IProjectionContainer projectionContainer)
        {
            var projectionContainerControl =
                new ProjectionContainerControl
                {
                    Title = projectionContainer.Name,
                    ItemsSource = GetProjectionControls(projectionContainer.Projections)
                };

            InitializeTransitions(projectionContainerControl, projectionContainer);
            return projectionContainerControl;
        }

        /// <summary>
        /// Get <see cref="ProjectionControl"/> instances for the given collection of <see cref="projections"/>
        /// </summary>
        /// <param name="projections">Collection of <see cref="IProjection"/> instances for which we need to create corresponding <see cref="ProjectionControl"/> instances</param>
        /// <returns>Collection of <see cref="ProjectionControl"/> instances created for <see cref="projections"/></returns>
        private IEnumerable<ProjectionControl> GetProjectionControls(IEnumerable<IProjection> projections)
        {
            var projectionControls = new List<ProjectionControl>();
            foreach(var projection in projections)
            {
                projectionControls.Add(
                    new ProjectionControl()
                        {
                            Name = projection.Name,
                            Content = projection.View
                        });
            }
            return projectionControls;
        }

        /// <summary>
        /// Initialize transitions of a given <see cref="projectionContainerControl"/> and a corresponding <see cref="projectionContainer"/>
        /// </summary>
        /// <param name="projectionContainerControl"><see cref="ProjectionContainerControl"/> that represents <see cref="projectionContainer"/></param>
        /// <param name="projectionContainer">Nothing, input <see cref="projectionContainerControl"/> gets initialized without any return value</param>
        private void InitializeTransitions(ProjectionContainerControl projectionContainerControl, IProjectionContainer projectionContainer)
        {
            if (projectionContainerControl == null) throw new ArgumentNullException("projectionContainerControl can't be null");
            if (projectionContainer == null) throw new ArgumentNullException("projectionContainer can't be null");
            
            foreach (var transition in projectionContainer.Transitions)
            {
                var sourceProjection = projectionContainer.GetProjection(transition.SourceProjectionName);
                if (sourceProjection == null)
                {
                    throw new ProjectionNotRegisteredException(projectionContainer, transition.SourceProjectionName);
                }

                var targetProjection = projectionContainer.GetProjection(transition.TargetProjectionName);
                if (targetProjection == null)
                {
                    throw new ProjectionNotRegisteredException(projectionContainer, transition.TargetProjectionName);
                }

                if (transition.Event == null)
                {
                    throw new NullReferenceException("Event for transition is not specified");
                }

                projectionContainerControl.Transitions.Add(new TransitionControl() { 
                    FromProjection = transition.SourceProjectionName,
                    ToProjection = transition.TargetProjectionName,
                    Event = transition.Event.Name
                });
            }
        }

        /// <summary>
        /// Initialize dictionaries, these are aimed at optimization of finding the proper projection elements
        /// </summary>
        private void InitializeDictionaries() 
        {
            this.container2ControlDictionary = new Dictionary<IProjectionContainer, ProjectionContainerControl>();
            this.control2ContainerDictionary = new Dictionary<ProjectionContainerControl, IProjectionContainer>();

            for (var i = 0; i < this.projectionContainersRepository.Count; i++) 
            {
                this.container2ControlDictionary.Add(this.projectionContainersRepository[i], this.projectionContainerControlsRepository[i]);
                this.control2ContainerDictionary.Add(this.projectionContainerControlsRepository[i], this.projectionContainersRepository[i]);
            }
        }
    }
}
